/*
 * clk.c - clocks basic code
 *
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/kernel.h>
#include <linux/clk.h>
#include <linux/clkdev.h>
#include <linux/clk-provider.h>
#include <linux/clk-private.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/spinlock.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/syscore_ops.h>
#include <mach/csp.h>
#include "clk.h"
#include "csp/lombo_clk_csp.h"

#ifdef CONFIG_PM_SLEEP
static LIST_HEAD(clock_reg_cache_list);
static DEFINE_SPINLOCK(clk_lock);
#endif

#ifdef CONFIG_OF

/**
 * of_divider_clk_setup - setup function for divider clock
 * @node: the matched device tree node in dts file
 *
 * the divider clock's config should be as below:
 * axi_clk {				the clock' name
 *	compatible = "divider-clock";	identify string, should not be changed
 *	reg = <0x0400a900 0x4>;		the register addr and len
 *	#clock-cells = <0>;		should be 0
 *	clocks = <&cpu>;		parent clock of axi_clk
 *	div-shift = <16>;		the bit offset for the divider
 *	div-width = <2>;		the bit length for the divider
 * };
 *
 */
void of_divider_clk_setup(struct device_node *node)
{
	const char *clk_name = node->name;
	const char *parent_name;
	u32 div_shift, div_width;
	struct clk *clk = NULL;
	unsigned long flags = 0;
	void __iomem *reg;

	reg = of_iomap(node, 0);
	WARN_ON(!reg);

	if (of_property_read_u32(node, "div-shift", &div_shift)
		|| of_property_read_u32(node, "div-width", &div_width)) {
		PRT_ERR("div-shift/div-width invalid, clock %s\n", clk_name);
		return;
	}

	parent_name = of_clk_get_parent_name(node, 0);
	if (NULL == parent_name) {
		PRT_ERR("parent clock config err, clock %s\n", clk_name);
		return;
	}

	clk = clk_register_divider(NULL, clk_name, parent_name, flags, reg,
			(u8)div_shift, (u8)div_width, 0, NULL);
	if (IS_ERR(clk)) {
		PRT_ERR("register clock %s failed\n", clk_name);
		return;
	}

	of_clk_add_provider(node, of_clk_src_simple_get, clk);
	WARN_ON(clk_register_clkdev(clk, clk_name, NULL));
}
EXPORT_SYMBOL_GPL(of_divider_clk_setup);
CLK_OF_DECLARE(divider_clk, "divider-clock", of_divider_clk_setup);

/**
 * of_gate_clk_setup - setup function for gate clock
 * @node: the matched device tree node in dts file
 *
 * apb1_gate {		node name(not the actual clock name), which be used by
 *				other module as "clocks = <&apb1_gate index>;"
 *	compatible = "gate-clock";	identify string, should not be changed
 *	reg = <0x0400a2c0 0x4>;		the register addr and len
 *	#clock-cells = <1>;		should be 1 (one para when used above)
 *	clocks = <&apb_clk>;		parent clock of apb1_gate
 *	clock-output-names = "apb_audio_gate", "apb_i2s_gate", ...;
 *				optional, string list of output clock names
 *	enable_bit_mask = <0x00000246>; bit mask for the reg, the bit sequence
 *					must be consistent with spec
 * };
 *
 */
void of_gate_clk_setup(struct device_node *node)
{
	const char *clk_name = node->name;
	struct clk_onecell_data *cod;
	const char *parent_name;
	struct clk *clk = NULL;
	void __iomem *reg;
	u32 bit_mask, last_bit_idx;
	int bit_idx, string_idx = 0, tmp;
	unsigned long flags = 0;
	unsigned long bit_val;

	if (is_ignore_unused_clk(clk_name))
		flags = CLK_IGNORE_UNUSED;

	reg = of_iomap(node, 0);
	WARN_ON(!reg);

	parent_name = of_clk_get_parent_name(node, 0);
	if (NULL == parent_name) {
		PRT_ERR("parent clock config err, clock %s\n", clk_name);
		return;
	}

	if (of_property_read_u32(node, "enable_bit_mask", &bit_mask)) {
		PRT_ERR("enable_bit_mask not found for clock %s\n", clk_name);
		return;
	}
	WARN_ON(0 == bit_mask);

	bit_val = (unsigned long)bit_mask;
	last_bit_idx = find_last_bit((const unsigned long *)&bit_val,
					BITS_PER_LONG);

	/* gate clocks without provider function */
	if (!of_get_property(node, "clock-output-names", &tmp)) {
		/* only one bit(the gate bit) should be set */
		WARN_ON(bit_mask & ~(1 << last_bit_idx));

		clk = clk_register_gate(NULL, clk_name, parent_name,
				0, reg, (u8)last_bit_idx, 0, NULL);

		WARN_ON(IS_ERR_OR_NULL(clk));
		WARN_ON(clk_register_clkdev(clk, clk_name, NULL));
		WARN_ON(of_clk_add_provider(node, of_clk_src_simple_get, clk));
		return;
	}

	/* gate clocks with provider function, such as ahb gate/reset */
	cod = kzalloc(sizeof(struct clk_onecell_data), GFP_KERNEL);
	WARN_ON(NULL == cod);

	cod->clks = kzalloc(sizeof(struct clk *) * (last_bit_idx + 1),
			GFP_KERNEL);
	WARN_ON(NULL == cod->clks);
	cod->clk_num = last_bit_idx + 1;

	for_each_set_bit(bit_idx, (const unsigned long *)&bit_mask,
				(unsigned long)(last_bit_idx + 1)) {
		tmp = of_property_read_string_index(node, "clock-output-names",
				string_idx, &clk_name);
		if (tmp) {
			PRT_ERR("cannot read clock-%d's name:%s\n", string_idx,
					clk_name);
			goto err;
		}

		if (is_ignore_unused_clk(clk_name))
			flags = CLK_IGNORE_UNUSED;

		clk = clk_register_gate(NULL, clk_name, parent_name,
				flags, reg, bit_idx, 0, NULL);
		if (IS_ERR(clk)) {
			PRT_ERR("register clock %s failed\n", clk_name);
			goto err;
		}

		cod->clks[string_idx] = clk;

		WARN_ON(clk_register_clkdev(clk, clk_name, NULL));
		string_idx++;
	}

	of_clk_add_provider(node, of_clk_src_onecell_get, cod);
	return;

err:
	if (NULL != cod->clks)
		kfree(cod->clks);
	if (NULL != cod)
		kfree(cod);
}
EXPORT_SYMBOL_GPL(of_gate_clk_setup);
CLK_OF_DECLARE(gate_clk, "gate-clock", of_gate_clk_setup);
#endif /* CONFIG_OF */

#ifdef CONFIG_PM_SLEEP

int lombo_clk_get_pm_flag(struct device_node *node, u32 *flag)
{
	const char *pm;

	of_property_read_string(node, "clock-pm", &pm);
	if (!strcmp(pm, "save")) {
		*flag |= LOMBO_CLK_SAVE;
		return 0;
	} else if (!strcmp(pm, "save_clear")) {
		*flag |= LOMBO_CLK_SAVE_CLEAR;
		return 0;
	} else
		return -EINVAL;
}

/**
 * lombo_clk_save: add clock register address for suspend/resume
 * @clk: clk handle to save
 * @base: clock register address to save
 * @disable: clock register claen or not(unused)
 */
int lombo_clk_save(struct clk *clk, void __iomem *base, bool clear)
{
	unsigned long flags;
	struct lombo_clk_reg_dump *reg_cache;

	list_for_each_entry(reg_cache, &clock_reg_cache_list, node) {
		if (reg_cache->addr == base)
			return 0;
	}

	reg_cache = kzalloc(sizeof(struct lombo_clk_reg_dump), GFP_KERNEL);
	if (!reg_cache) {
		pr_err("malloc for backup register 0x%08x failed.\n",
			(u32)base);
		return -ENOMEM;
	}
	pr_debug("save clock[%s] register: 0x%08x\n", __clk_get_name(clk), (u32)base);

	spin_lock_irqsave(&clk_lock, flags);
	if (!clk) {
		spin_unlock_irqrestore(&clk_lock, flags);
		return -ENODEV;
	}
	reg_cache->clk = clk;
	reg_cache->addr = base;
	reg_cache->clear = clear;
	list_add_tail(&reg_cache->node, &clock_reg_cache_list);
	spin_unlock_irqrestore(&clk_lock, flags);

	return 0;
}

static int lombo_clk_suspend(void)
{
	unsigned long flags;
	struct lombo_clk_reg_dump *reg_cache;

	spin_lock_irqsave(&clk_lock, flags);
	list_for_each_entry(reg_cache, &clock_reg_cache_list, node) {
		reg_cache->value = readl(reg_cache->addr);
		pr_debug("backup[%s] 0x%08x  -- 0x%08x\n", __clk_get_name(reg_cache->clk),
				(u32)reg_cache->addr, reg_cache->value);
		if (reg_cache->clear)
			writel(0x00, reg_cache->addr);
	}
	spin_unlock_irqrestore(&clk_lock, flags);

	return 0;
}

static void lombo_clk_resume(void)
{
	unsigned long flags;
	struct lombo_clk_reg_dump *reg_cache;

	spin_lock_irqsave(&clk_lock, flags);
	list_for_each_entry(reg_cache, &clock_reg_cache_list, node) {
		pr_debug("resume [0x%08x] -- 0x%08x from 0x%08x\n",
			(u32)reg_cache->addr, reg_cache->value,
			readl(reg_cache->addr));
		writel(reg_cache->value, reg_cache->addr);
	}
	spin_unlock_irqrestore(&clk_lock, flags);
}

static struct syscore_ops lombo_clk_syscore_ops = {
	.suspend = lombo_clk_suspend,
	.resume = lombo_clk_resume,
};
#endif /* CONFIG_PM_SLEEP */

static void __sync_prepare_clk(const char *clk_name)
{
	struct clk *clk = NULL;
	int ret;

	PRT_INFO(">>>>>sync u-boot clk: %s\n", clk_name);

	/* get clock handle */
	clk = clk_get(NULL, clk_name);
	if (!IS_ERR(clk)) {
		/* enable the clock */
		ret = clk_prepare_enable(clk);
		if (ret != 0)
			PRT_ERR("clk_prepare_enable clk failed: %s\n",
				clk_name);

		/* put clock handle */
		clk_put(clk);
	} else
		PRT_ERR("clk_get failed: %s\n", clk_name);
}

static int __get_clk_en_bit(u32 reg_addr, u32 b)
{
	u32 rdval;

	rdval = READREG32(reg_addr);
	return rdval & BIT(b);
}

static void __sync_uboot_disp_clk(void)
{
	u32 rdval;
	u32 reg_addr;
	char *clk_name;

	PRT_INFO(">>>>>sync uboot disp clk\n");

	/* ahb_dpu_gate */
	if (__get_clk_en_bit(VA_PRCM_AHB_GAT1, 2)) {
		clk_name = "ahb_dpu_gate";
		__sync_prepare_clk(clk_name);
	}

	/* ahb_dpu_reset */
	if (__get_clk_en_bit(VA_PRCM_AHB_RST1, 2)) {
		clk_name = "ahb_dpu_reset";
		__sync_prepare_clk(clk_name);
	}

	/* maxi_dpu_gate */
	if (__get_clk_en_bit(VA_PRCM_MAXI_GAT, 8)) {
		clk_name = "maxi_dpu_gate";
		__sync_prepare_clk(clk_name);
	}

	/* perh0_pll_div2 */
	reg_addr = VA_PRCM_PERH0_PLL_EN;
	rdval = READREG32(reg_addr);
	if ((rdval & BIT(0)) && (rdval & BIT(1)) && (rdval & BIT(2))) {
		clk_name = "perh0_pll_div2";
		__sync_prepare_clk(clk_name);
	}

	/* dpu_sclk0 */
	if (__get_clk_en_bit(VA_PRCM_DPU_SCLK0_CLK_CTRL, 31)) {
		clk_name = "dpu_sclk0";
		__sync_prepare_clk(clk_name);
	}

	/* dpu_sclk2 */
	/*
	 * if (__get_clk_en_bit(VA_PRCM_DPU_SCLK2_CLK_CTRL, 31)) {
	 *         clk_name = "dpu_sclk2";
	 *         __sync_prepare_clk(clk_name);
	 * }
	 */

}

/**
 * lombo_clk_init - intialize the pll & gate &.. clocks
 *
 */
int __init lombo_clk_init(void)
{
	struct clk *clk = NULL;
	int i;

	for (i = 0; i < clk_to_en_counts; i++) {
		/* get clock handle */
		clk = clk_get(NULL, clk_to_en[i]);
		if (IS_ERR(clk)) {
			PRT_ERR("get clk %s handle failed, %p\n", clk_to_en[i], clk);
			continue;
		}

		/* enable the clock */
		PRT_INFO("clk_prepare_enable clk: %s\n", clk_to_en[i]);
		if (clk_prepare_enable(clk))
			PRT_ERR("prepare enable %s failed\n", clk_to_en[i]);

		/* put clock handle */
		clk_put(clk);
	}

	__sync_uboot_disp_clk();

#ifdef CONFIG_PM_SLEEP
	if (!list_empty(&clock_reg_cache_list))
		register_syscore_ops(&lombo_clk_syscore_ops);
	else
		PRT_ERR("register pm syscore ops for clock failed\n");
#endif

	PRT_INFO("lombo clk init finished\n");
	return 0;
}

core_initcall(lombo_clk_init);

